Python 3.6 has its release schedule posted in PEP 494.
I'm interested in having this information into my calendar. Rather than the sensible thing of manually adding a few events to my calendar, I'm going to build a calendar automatically in a notebook.
Why? Because notebooks are fun, and I've been using soupy to practice some functional programming.
I'm going to use soupy to extract the information, and icalendar to build the calendar data.
import requests
from soupy import Soupy, Q
Fetch the page:
url = 'https://www.python.org/dev/peps/pep-0494/'
soup = Soupy(requests.get(url).text, 'html.parser')
The schedule is helpfully encapsulated in div#schedule
:
print(soup.find(id='schedule').val())
<div class="section" id="schedule"> <h2> <a class="toc-backref" href="#id6"> 3.6.0 schedule </a> </h2> <ul class="simple"> <li> 3.6 development begins: 2015-05-24 </li> <li> 3.6.0 alpha 1: 2016-05-17 </li> <li> 3.6.0 alpha 2: 2016-06-13 </li> <li> 3.6.0 alpha 3: 2016-07-11 </li> <li> 3.6.0 alpha 4: 2016-08-15 </li> <li> 3.6.0 beta 1: 2016-09-12 (was 09-07) </li> </ul> <p> (No new features beyond this point.) </p> <ul class="simple"> <li> 3.6.0 beta 2: 2016-10-03 </li> <li> 3.6.0 beta 3: 2016-10-31 </li> <li> 3.6.0 beta 4: 2016-11-21 </li> <li> 3.6.0 candidate 1: 2016-12-05 </li> <li> 3.6.0 candidate 2 (if needed): 2016-12-12 </li> <li> 3.6.0 final: 2016-12-16 </li> </ul> </div>
We can see that each entry is a pretty simple <li>
tag,
with a release and date separated by a colon.
We can use Soupy's functional style to quickly parse this information. We want to do:
#schedule
li
in #schedule
:':'
raw_data = soup \
.find(id='schedule') \
.find_all('li') \
.each(
Q.text.strip().split(':')
)
raw_data.val()
[['3.6 development begins', ' 2015-05-24'], ['3.6.0 alpha 1', ' 2016-05-17'], ['3.6.0 alpha 2', ' 2016-06-13'], ['3.6.0 alpha 3', ' 2016-07-11'], ['3.6.0 alpha 4', ' 2016-08-15'], ['3.6.0 beta 1', ' 2016-09-12 (was 09-07)'], ['3.6.0 beta 2', ' 2016-10-03'], ['3.6.0 beta 3', ' 2016-10-31'], ['3.6.0 beta 4', ' 2016-11-21'], ['3.6.0 candidate 1', ' 2016-12-05'], ['3.6.0 candidate 2 (if needed)', ' 2016-12-12'], ['3.6.0 final', ' 2016-12-16']]
Let's parse those dates before we get ahead of ourselves:
import datetime
import re
date_pat = re.compile(r'\d{4}-\d{2}-\d{2}')
def parse_date(title_date):
title, date_string = title_date
date_part = date_pat.search(date_string).group()
date = datetime.datetime.strptime(date_part, '%Y-%m-%d').date()
return (title, date)
data = raw_data.each(Q.map(parse_date))
data.val()
[('3.6 development begins', datetime.date(2015, 5, 24)), ('3.6.0 alpha 1', datetime.date(2016, 5, 17)), ('3.6.0 alpha 2', datetime.date(2016, 6, 13)), ('3.6.0 alpha 3', datetime.date(2016, 7, 11)), ('3.6.0 alpha 4', datetime.date(2016, 8, 15)), ('3.6.0 beta 1', datetime.date(2016, 9, 12)), ('3.6.0 beta 2', datetime.date(2016, 10, 3)), ('3.6.0 beta 3', datetime.date(2016, 10, 31)), ('3.6.0 beta 4', datetime.date(2016, 11, 21)), ('3.6.0 candidate 1', datetime.date(2016, 12, 5)), ('3.6.0 candidate 2 (if needed)', datetime.date(2016, 12, 12)), ('3.6.0 final', datetime.date(2016, 12, 16))]
Now that we have the data, we can build the calendar with icalendar.
We want each release to be an all-day event, and have a summary like "Python release 3.6.0 beta 1". For good behavior reasons, we will give each item a UID, so that importing the same event multiple times doesn't create duplicates.
import hashlib
import icalendar
cal = icalendar.Calendar()
def add_event(cal, title, date):
evt = icalendar.Event()
# add Python to the summary
summary = 'Python ' + title
evt.add('summary', 'Python ' + title)
evt.add('dtstart', date)
evt.add('dtend', date)
# give it a UID for stability on repeated imports
key = b'python-release-%s' % title.encode('utf8')
evt.add('uid', hashlib.md5(key).hexdigest())
cal.add_component(evt)
data.each(Q.map(lambda title_date: add_event(cal, *title_date)))
print(cal.to_ical().decode())
BEGIN:VCALENDAR BEGIN:VEVENT SUMMARY:Python 3.6 development begins DTSTART;VALUE=DATE:20150524 DTEND;VALUE=DATE:20150524 UID:67825e3ff3754a56e4f7d2c647ff0921 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 alpha 1 DTSTART;VALUE=DATE:20160517 DTEND;VALUE=DATE:20160517 UID:56e8815e1b8f2ee4adcd64c097685d40 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 alpha 2 DTSTART;VALUE=DATE:20160613 DTEND;VALUE=DATE:20160613 UID:1761e57b86af70d47184b8fb0bbab671 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 alpha 3 DTSTART;VALUE=DATE:20160711 DTEND;VALUE=DATE:20160711 UID:183cbb8704be0e84ee59163a4b3345dc END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 alpha 4 DTSTART;VALUE=DATE:20160815 DTEND;VALUE=DATE:20160815 UID:56ca94d86e853816206b2d4d199ebb03 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 beta 1 DTSTART;VALUE=DATE:20160912 DTEND;VALUE=DATE:20160912 UID:81dd90786b6a3d4604b12e3edf5ca95a END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 beta 2 DTSTART;VALUE=DATE:20161003 DTEND;VALUE=DATE:20161003 UID:1393eb7d31ed6cf473d8f11d77d16035 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 beta 3 DTSTART;VALUE=DATE:20161031 DTEND;VALUE=DATE:20161031 UID:be9bf8af73fa4e958da3499936565e64 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 beta 4 DTSTART;VALUE=DATE:20161121 DTEND;VALUE=DATE:20161121 UID:ddc83f39cb269352bc088e11e633212f END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 candidate 1 DTSTART;VALUE=DATE:20161205 DTEND;VALUE=DATE:20161205 UID:1c3e6cf5093809b0176529b9ea60bf99 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 candidate 2 (if needed) DTSTART;VALUE=DATE:20161212 DTEND;VALUE=DATE:20161212 UID:d285018665315ba07a0b1fad4ff89100 END:VEVENT BEGIN:VEVENT SUMMARY:Python 3.6.0 final DTSTART;VALUE=DATE:20161216 DTEND;VALUE=DATE:20161216 UID:8927cac2581ac4a054994fc00dea469c END:VEVENT END:VCALENDAR
Now we can save this file to disk:
with open('python36.ics', 'wb') as f:
f.write(cal.to_ical())
and import the resulting python36.ics
into your calendar application of choice.
You can see the result of importing this file into Google Calendar:
%%html
<iframe src="https://calendar.google.com/calendar/embed?src=arcfq33k7n1docj2bpbda8jhg0%40group.calendar.google.com" style="border: 0" width="800" height="600" frameborder="0" scrolling="no"></iframe>